namespace tetris {
    interface IEventMap {
        [type: string]: Array<(data?: any) => any>;
    }

    export interface IEvent {
        type: string;
        sender: any;
        handled?: boolean;
        data?: any;
        [name: string]: any;
    }

    export abstract class Eventable {
        private _events: IEventMap;

        protected constructor() {
            this._events = $({});
        }

        on(type: string, handler: (data?: any) => any): this {
            type = type.toLowerCase();
            (this._events[type] || (this._events[type] = [])).push(handler);
            return this;
        }

        off(type: string): this {
            delete this._events[type.toLowerCase()];
            return this;
        }

        trigger(event: IEvent): this;
        trigger(type: string, data?: any, isExtend?: boolean): this;
        trigger(e: any, data?: any, isExtend: boolean = true): this {
            const event: IEvent = typeof e === "string"
                ? {
                    type: e.toLowerCase(),
                    sender: this
                }
                : e as IEvent;

            const handlerList = this._events[event.type];
            if (!handlerList) {
                return this;
            }

            if (isExtend && data && typeof data === "object") {
                Object.keys(data).forEach(key => {
                    if (!/^(?:type|sender)$/.test(key)) {
                        event[key] = data[key];
                    }
                });
            } else if (data !== void 0 && data !== null) {
                event.data = data;
            }

            for (let handler of handlerList) {
                handler(event);
                if (event.handled) {
                    break;
                }
            }
            return this;
        }
    }
}
